跳到主要内容

Go 权限校验库 casbin

Casbin 是一个强大的、高效的开源访问控制框架,其权限管理机制支持多种访问控制模型。

Casbin 是什么?

casbin 是一个强大、高效的访问控制库。支持常用的多种访问控制模型,如ACL/RBAC/ABAC 等。可以实现灵活的访问权限控制。

权限实际上就是控制谁能对什么资源进行什么操作。casbin 将访问控制模型抽象到一个基于 PERM(Policy,Effect,Request,Matchers) 元模型的配置文件(模型文件)中。因此切换或更新授权机制只需要简单地修改配置文件。

  • policy 是策略或者说是规则的定义。它定义了具体的规则。
  • request 是对访问请求的抽象,它与 e.Enforce() 函数的参数是一一对应的
  • matcher 匹配器会将请求与定义的每个 policy 一一匹配,生成多个匹配结果。
  • effect 根据对请求运用匹配器得出的所有结果进行汇总,来决定该请求是允许还是拒绝。

以下举一个例子:

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _ , _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)

整个过程和 SpringSecurity 的那个模式是一样的

20221020160951

Casbin 可以

支持自定义请求的格式,默认的请求格式为 {subject, object, action}。 具有访问控制模型 model 和策略 policy 两个核心概念。 支持 RBAC 中的多层角色继承,不止主体可以有角色,资源也可以具有角色。 支持内置的超级用户 例如:root 或 administrator。超级用户可以执行任何操作而无需显式的权限声明。 支持多种内置的操作符,如 keyMatch,方便对路径式的资源进行管理,如 /foo/bar 可以映射到 /foo*

Casbin 不能

身份认证 authentication(即验证用户的用户名、密码),casbin 只负责访问控制。应该有其他专门的组件负责身份认证,然后由 casbin 进行访问控制,二者是相互配合的关系。 管理用户列表或角色列表。 Casbin 认为由项目自身来管理用户、角色列表更为合适, 用户通常有他们的密码,但是 Casbin 的设计思想并不是把它作为一个存储密码的容器。而是存储 RBAC 方案中用户和角色之间的映射关系。

模型文件说明

一个模型文件如下所示,它可以用来定义访问控制模型。

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _ , _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)

sub, obj, act 表示经典三元组:访问实体 (Subject),访问资源 (Object) 和访问方法 (Action)。

  • sub:用户(角色)
  • obj:访问路径(访问的资源)
  • act:访问方法(GET、POST、PUT、DELETE)

对请求的定义 request

首先是对请求的定义,它定义了 e.Enforce(...) 函数中的参数。

[request_definition]
r = sub, obj, act

访问实体 (Subject),访问资源 (Object) 和访问方法 (Action)

譬如 A1 这个用户 要访问 /usersGET 请求,模型文件 r = sub, obj, act 是这样的,那么你的策略文件就写成:A1,/users,GET

// 下面校验了 alice 用户是否可以访问 /users GET
sub := "alice"
obj := "/users"
act := "GET"

// 注意这个 Enforce 它是一个可变参数,它的参数顺序和上面的 r = sub, obj, act 一致,
// 但是这个 r 是可以自己定义的,所以模型文件的定义决定了这里的参数顺序
if res, _ := e.Enforce(sub, obj, act); !res {
log.Fatalf("unexpected enforce result")
}

对策略的定义 policy

这个 [policy_definition] 是策略的定义。它界定了该策略的含义。例如,我们有以下模式:

[policy_definition]
p = sub, obj, act
p2 = sub, act

这些是我们对 policy 规则的具体描述

p, alice, data1, read
p2, bob, write-all-objects

效果一样,它是对策略的定义

policy 部分的每一行称之为一个策略规则, 每条策略规则通常以形如 p, p2 的 policy type 开头。 如果存在多个 policy 定义,那么我们会根据前文提到的 policy type 与具体的某条定义匹配。

上面的 policy 的绑定关系将会在 matcher 中使用, 罗列如下:

(alice, data1, read) -> (p.sub, p.obj, p.act)
(bob, write-all-objects) -> (p2.sub, p2.act)

匹配器的定义 matcher

[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act

这个是请求和策略的匹配规则,上面这句,就是请求的用户 p.sub 、资源 p.obj、请求方式 p.act,必须完全等于策略文件中定义的用户 r.sub 、资源 r.obj、请求方式 r.act。

匹配器的函数使用

情况一:在这里还可以使用 casbin 自定义一种匹配函数,比如说:一个访问路径是:/alice_data/:resource,这是 gin 中的标准路径,后面的 :resource 代表传递的参数。那么你的匹配要改为

m = r.sub == p.sub && keyMatch2(r.obj, p.obj) && r.act == p.act。

情况二:同一个 deptadmin 角色访问 /user 可以 post 也可以 get,按照原先的定义在策略文件中要写两条

p deptadmin /depts GET 
p deptadmin /depts POST

其实我们可以简化为 p deptadmin /depts (GET|POST)

这种情况下可以使用正则自定义函数匹配

func init() {
E.AddFunction("methodMatch", func(args ...interface{}) (interface{}, error) {
if len(args) == 2 {
k1, k2 := args[1].(string), args[2].(string)
return MethodMatch(k1, k2), nil
}
return nil, fmt.Errorf("methodMatch error")
})
}

func MethodMatch(key1 string, key2 string) bool {
ks := strings.Split(key2, "|")
for _, k := range ks {
if k == key1 {
return true
}
}
return false
}

然后在策略文件中写上

m = r.sub == p.sub && keyMatch2(r.obj,p.obj) && methodMatch(r.act,p.act)

策略的生效范围

[policy_effect] 部分是对 policy 生效范围的定义,即定义了当多个 policy rule 同时匹配访问请求 request 时,该如何对多个决策结果进行集成以实现统一决策。

以下示例展示了一个只有一条规则生效,其余都被拒绝的情况:

[policy_effect]
e = some(where (p.eft == allow))

该 Effect 原语表示如果存在任意一个决策结果为 allow 的匹配规则,则最终决策结果为 allow,即 allow-override。

提示
  • some 量词判断是否存在一条策略规则满足匹配器。
  • any 量词则判断是否所有的策略规则都满足匹配器 (此处未使用)。

其中 p.eft 表示策略规则的决策结果,可以为 allow 或者 deny,当不指定规则的决策结果时,取默认值 allow。 通常情况下, policy 的 p.eft 默认为 allow, 因此前面例子中都使用了这个默认值。

这是另一个 policy effect 的例子:

[policy_effect]
e = !some(where (p.eft == deny))

该 Effect 原语表示不存在任何决策结果为 deny 的匹配规则,则最终决策结果为 allow ,即 deny-override。

[policy_effect]
e = some(where (p.eft == allow)) && !some(where (p.eft == deny))

该 Effect 原语表示当至少存在一个决策结果为 allow 的匹配规则,且不存在决策结果为 deny 的匹配规则时,则最终决策结果为allow。 这时 allow 授权和 deny 授权同时存在,但是 deny 优先。

实际使用例子:

[policy_effect]
e = some(where (p.eft == allow))

对 policy 生效范围的定义,上面表示:如果存在任意一个决策结果为 allow 的匹配规则,则最终决策结果为 allow,这个 p.eft 就是决策结果

这里我们可以设置特例,比如角色 A1,对 /user post 有权限,张三、李四都属于 A1。一般情况下,上面的范围定义就对了,只要 A1 有 allow 权限,张三李四都可以访问,但是如果遇到一个特例,我们不想给李四 /user post 的权限,只给张三,其他 A1 权限都给,这种情况下,我们在策略文件中将写上李四,/user,post,deny。然后生效范围的定义就必须改成 e = some(where (p.eft == allow)) && !some(where (p.eft == deny))

但是注意!! 上面的看起来是有语法,但是实际上它是写死的硬编码,只能用定义好的几个规则,可以看源码里的定义:

casbin/casbin/constant/constants.go

所以实际只能用下面的几个定义:

Policy effect定义意义示例
some(where (p.eft == allow))allow-overrideACL, RBAC, etc.
!some(where (p.eft == deny))deny-override拒绝改写
some(where (p.eft == allow)) && !some(where (p.eft == deny))allow-and-deny同意与拒绝
`priority(p.eft)deny`
subjectPriority(p.eft)基于角色的优先级主题优先级

可以看这里提供的例子: https://casbin.org/zh/docs/supported-models#examples

对角色的定义

[role_definition] 原语定义了 RBAC 中的角色继承关系。

[role_definition]
g = _, _
提示

_, _ 表示角色继承关系的前项和后项,即前项继承后项角色的权限,简单来说,就是前项拥有后项的全部权限。

例如 admin >= member >= guest 最后的理解就是前包含后,并且会有后项没有的独特权限。后项的权限肯定比前少。

在 Casbin 里,我们以 policy 表示中实际的用户角色映射关系 (或是资源-角色映射关系),例如:

p, data2_admin, data2, read
g, alice, data2_admin

这意味着 alice 是角色 data2_admin 的一个成员。alice 在这里可以是用户、资源或角色。 Cabin 只是将其识别为一个字符串。

接下来在 matcher 中,应该像下面的例子一样检查角色信息:

[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

这意味着在请求中应该在 policy 中包含 sub 角色。

多个 RBAC 系统

Casbin 支持 RBAC 系统的多个实例,例如,用户可以有角色和继承关系,而资源也可以有角色和继承关系。 这两个RBAC系统不会干扰。

[role_definition]
g = _, _
g2 = _, _

上述角色定义表明,g 是一个 RBAC系统,g2 是另一个 RBAC 系统

引入域的概念

如果还需要再引入域这个概念,可以使用下面这样的写法

[role_definition]
g = _, _, _

第三个 _ 表示域/租户的名称, 此部分不应更改。 然后,政策可以是:

p, admin, tenant1, data1, read
p, admin, tenant2, data2, read

g, alice, admin, tenant1
g, alice, user, tenant2

该实例表示 tenant1 的域内角色 admin 可以读取 data1, alice 在 tenant1 域中具有 admin 角色,但在 tenant2 域中具有 user 角色, 所以 alice 可以有读取 data1 的权限。 同理,因为 alice 不是 tenant2 的 admin,所以她访问不了 data2。

接下来在 matcher 中,应该像下面的例子一样检查角色信息:

[matchers]
m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act

上下文切换

在之前的模型推导过程中,模型总是定义了不止一个策略,不止一个匹配器,那么怎么在代码中体现呢?

例如下面的例子,它同时定义了 RABC 和 ABAC 两种模式

会发现每个对象都多了一份,对应的策略文件也是多了一份,这是因为 Casbin 支持上下文切换,也就是说,我们可以在代码中指定使用哪个策略文件,哪个匹配器,这样就可以实现不同的策略文件,不同的匹配器,不同的模型,不同的权限管理模式。

它也定义了两个策略分别为策略p, p2定义不同的策略,需要的参数不同

所以就需要使用上下文 EnforceContext 进行切换

func main() {
conf := `
[request_definition]
r = sub, obj, act
r2 = sub, obj, act

[policy_definition]
p = sub, obj, act
p2= sub_rule, obj, act, eft

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
#RABC
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
#ABAC
m2 = eval(p2.sub_rule) && r2.obj == p2.obj && r2.act == p2.act
`

line := `
p, data2_admin, data2, read
p2, r2.sub.Age > 18 && r2.sub.Age < 60, /data1, read, allow
p2, r2.sub.Age > 60 && r2.sub.Age < 100, /data1, read, deny
g, alice, data2_admin
`

a := stringadapter.NewAdapter(line)
m := model.NewModel()
err := m.LoadModelFromText(conf)
if err != nil {
log.Fatalf("could not load model from text: %v", err)
}

e, _ := casbin.NewEnforcer(m, a)

sub := "data2_admin"
obj := "data2"
act := "read"
if res, _ := e.Enforce(sub, obj, act); !res {
log.Fatalf("unexpected enforce result")
}

enforceContext := casbin.NewEnforceContext("2")
enforceContext.EType = "e"
sub2 := struct{ Age int }{Age: 25}
obj2 := "/data1"
act2 := "read"
if res, _ := e.Enforce(enforceContext, sub2, obj2, act2); !res {
log.Fatalf("unexpected enforce result")
}
}

策略文件说明

策略文件是一个CSV文件,可以跟模型文件一样,在初始化的时候加载,

e, err := casbin.NewEnforcer("模型文件路径", "策略文件路径")

Casbin 不生产策略,这个策略我们一般开发时,需要在程序中由用户来进行定义,Casbin 只是将用户定义的数据,持久化到数据库进行保存,方便资源访问时进行权限判断。

文本写法

20221027102656

数据库保存:

20221027102745

DDL 语句

DROP TABLE IF EXISTS `casbin_rule`;
CREATE TABLE `casbin_rule` (
`ptype` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`v0` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`v1` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`v2` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`v3` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`v4` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`v5` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = COMPACT;

Reference